<?php

namespace Henan\ThinkSdk\traits;


use Henan\ThinkSdk\helper\FC;
use Henan\ThinkSdk\utils\TreeUtil;
use think\db\exception\DataNotFoundException;
use think\db\exception\DbException;
use think\db\exception\ModelNotFoundException;
use think\Exception;
use think\facade\Db;
use think\Model;

/**
 * CRUD控制器复用特征
 * @author henan
 */
trait CrudTrait
{
    use ValidateTrait;
    use ResponseTrait;

    /**
     * 当前模型
     * @var Model
     */
    protected Model $model;

    /**
     * 主键
     * @var string
     */
    protected string $primaryKey = 'id';

    /**
     * 外键
     * @var array
     */
    protected array $foreignKey = [];

    /**
     * 字段排序（默认按ID降序排序）
     * @var array|string[]
     */
    protected array $sort = ['id' => 'desc'];

    /**
     * 唯一字段
     * @var array
     */
    protected array $uniqueFields = [];

    /**
     * 允许更新的字段(注意：设置后不允许更新字段无效)
     * @var array
     */
    protected array $allowUpdateFields = [];

    /**
     * 不允许更新的字段(注意：当未设置允许更新字段时有效)
     * @var array|string[]
     */
    protected array $withoutUpdateFields = ['create_time', 'delete_time', 'update_time'];

    /**
     * 导出的字段
     * @var array
     */
    protected array $fields = [];

    /**
     * 不导出的字段
     * @var array|string[]
     */
    protected array $withoutFields = ['delete_time'];

    /**
     * 预载入查询
     * @var array
     */
    protected array $with = [];

    /**
     * 隐藏字段
     * @var array
     */
    protected array $hidden = [];

    /**
     * 追加字段
     * @var array
     */
    protected array $append = [];

    /**
     * 查询条件
     * @var array
     */
    protected array $where = [];

    /**
     * 写入参数验证规则
     * @var array
     */
    protected array $writeRule = [];

    /**
     * 写入前事件
     * @param $param
     * @return void
     * @throws DataNotFoundException
     * @throws DbException
     * @throws ModelNotFoundException
     */
    protected function writeBefore($param): void
    {
        // 检查字段是否允许重复
        if ($this->uniqueFields) {
            foreach ($this->uniqueFields as $key => $value) {
                $field = $key;
                $msg = $value;
                if (is_int($key)) {
                    $field = $value;
                    $msg = $field . '不能重复,' . $param[$field] . '已存在';
                }
                if (isset($param[$field])) {
                    $where[] = [$field, '=', $param[$field]];
                    isset($param['id']) && $where[] = ['id', '<>', $param['id']];
                    $check = $this->model->where($where)->find();
                    $check && $this->error($msg);
                }
            }
        }
    }

    /**
     * 构建查询条件
     * @param $filter
     * @param $op
     * @return array
     */
    protected function buildWhere($filter, $op): array
    {
        empty($filter) && $filter = [];
        empty($op) && $op = [];
        $where = [];
        foreach ($filter as $key => $val) {
            $op = !empty($op[$key]) ? $op[$key] : '%*%';
            switch (strtolower($op)) {
                case '=':
                    $where[] = [$key, '=', $val];
                    break;
                case '%*%':
                    $where[] = [$key, 'LIKE', "%{$val}%"];
                    break;
                case '*%':
                    $where[] = [$key, 'LIKE', "{$val}%"];
                    break;
                case '%*':
                    $where[] = [$key, 'LIKE', "%{$val}"];
                    break;
                case 'range':
                    [$beginTime, $endTime] = explode(' - ', $val);
                    $where[] = [$key, '>=', strtotime($beginTime)];
                    $where[] = [$key, '<=', strtotime($endTime)];
                    break;
                case 'find in set':
                    $where[] = [$key, 'find in set', $val];
                    break;
                default:
                    $where[] = [$key, $op, "%{$val}"];
            }
        }
        return $where;
    }

    /**
     * 选择框
     * @param array $fields 字段 ['id'=>'value', 'name'=>'label'] 或 ['id','name']
     * @param array $joinField 拼接字段 ['label'=>'id'], 会自动拼接成 label(id), 必须是键值对
     * @return void 响应输出
     */
    protected function crudSelect(array $fields = ['id' => 'value', 'name' => 'label'], array $joinField = []): void
    {
        try {
            if ($this->foreignKey) {
                $this->where = array_merge($this->where, array_map(
                    fn($v, $k) => [$k, '=', $v],
                    $this->foreignKey,
                    array_keys($this->foreignKey)
                ));
            }
            $isAssoc = FC::arrIsAssoc($fields);
            $columns = $isAssoc ? array_keys($fields) : $fields;
            $rows = $this->model->where($this->where)->column($columns);
            $data = array_map(function ($row) use ($fields, $joinField, $isAssoc) {
                $item = [];
                foreach ($row as $col => $val) {
                    $key = $isAssoc ? ($fields[$col] ?? $col) : $col;
                    if ($joinField && isset($joinField[$key], $row[$joinField[$key]])) {
                        $val .= "({$row[$joinField[$key]]})";
                    }
                    $item[$key] = $val;
                }
                return $item;
            }, $rows ?: []);
        } catch (\Exception $e) {
            $this->error($e->getMessage());
        }
        $this->success($data);
    }

    /**
     * 列表
     * @param bool $isPaging 是否分页
     * @param callable|null $afterFun 查询后回调函数
     * @return void 响应输出
     */
    protected function crudList(bool $isPaging = true, callable $beforeFun = null, callable $afterFun = null): void
    {
        $param = $this->check(['page|integer' => 1, 'limit|integer' => 10, 'filter' => '', 'op' => '', 'order' => '']);
        try {
            // 执行前置回调函数
            if ($beforeFun) $param = $beforeFun($param);
            if (!is_array($param['filter'])) $param['filter'] = json_decode($param['filter'], true);
            if (!is_array($param['op'])) $param['op'] = json_decode($param['op'], true);
            $sort = $this->sort;
            if (!empty($param['order'])) {
                if (is_array($param['order'])) {
                    $sort = $param['order'];
                } else {
                    $arr = explode(',', $param['order']);
                    $field = $arr[0];
                    $order = $arr[1] ?? 'asc';
                    $sort = [$field => $order];
                }
            }
            // 构建查询条件
            $where = $this->buildWhere($param['filter'], $param['op']);
            // 附加配置查询条件
            $where = array_merge($where, $this->where);
            // 附加配置外键查询条件
            if ($this->foreignKey) foreach ($this->foreignKey as $key => $value) $where[] = [$key, '=', $value];
            // 构建条件查询器
            $query = $this->model->where($where);
            // 查询总条数
            $count = $query->count();
            // 查询数据
            $query = $query->with($this->with)->hidden($this->hidden)->append($this->append)->order($sort);
            $query = $this->fields ? $query->field($this->fields) : $query->withoutField($this->withoutFields);
            $list = $isPaging ? $query->page($param['page'], $param['limit'])->select()->toArray() : $query->select()->toArray();
            // 执行后置回调函数
            if ($afterFun) $list = $afterFun($list);
        } catch (\Exception $e) {
            $this->error($e->getMessage());
        }
        if ($isPaging) {
            $pages = (int)(($count + $param['limit'] - 1) / $param['limit']);
            $list = ['count' => $count, 'pages' => $pages, 'page' => $param['page'], 'limit' => $param['limit'], 'list' => $list];
        }
        $this->success($list);
    }

    /**
     * 创建
     * @param array $rule 自定义验证规则
     * @param callable|null $beforeFun 创建前回调函数
     * @param callable|null $afterFun 创建后回调函数
     * @return void 响应输出
     */
    protected function crudCreate(array $rule = [], callable $beforeFun = null, callable $afterFun = null): void
    {
        $param = $this->check(array_merge($rule, $this->writeRule), 'post');
        empty($param) && $this->error('请求参数不能为空');
        Db::startTrans();
        try {
            // 执行前置回调函数
            if ($beforeFun) $param = $beforeFun($param);
            // 执行写入前方法
            $this->writeBefore($param);
            // 建外键参数附加到请求参数中
            if ($this->foreignKey) foreach ($this->foreignKey as $key => $value) $param[$key] = $value;
            // 创建数据
            $save = $this->model::create($param);
            // 执行后置回调函数
            if ($afterFun) $afterFun($save, $param);
            Db::commit();
        } catch (\Exception $e) {
            Db::rollback();
            $this->error('创建失败:' . $e->getMessage());
        }
        $this->success($save, '创建成功');
    }

    /**
     * 更新
     * @param array $rule
     * @param callable|null $beforeFun 更新前回调函数
     * @param callable|null $afterFun 更新后回调函数
     * @return void 响应输出
     */
    protected function crudUpdate(array $rule = [], callable $beforeFun = null, callable $afterFun = null): void
    {
        $KEY = $this->primaryKey;
        if (!isset($rule[$KEY])) $rule[] = $KEY;
        $param = $this->check(array_merge($rule, $this->writeRule), 'put');
        empty($param) && $this->error('请求参数不能为空');
        Db::startTrans();
        try {
            // 执行前置回调函数
            if ($beforeFun) $param = $beforeFun($param);
            // 查询数据
            $row = $this->model->find($param[$this->primaryKey]);
            if (empty($row)) throw new Exception('数据不存在');
            // 将外键设为不可更新字段
            if ($this->foreignKey) foreach ($this->foreignKey as $key => $value) $this->withoutUpdateFields[] = $key;
            // 检查字段是否允许更新
            foreach ($param as $key => $value) {
                if ($key == $this->primaryKey) continue;
                if ($this->allowUpdateFields) {
                    if (!in_array($key, $this->allowUpdateFields) && $value != $row[$key]) throw new Exception("{$key}字段不允许更新");
                } else {
                    if ($this->withoutUpdateFields) {
                        if (in_array($key, $this->withoutUpdateFields) && $value != $row[$key]) throw new Exception("{$key}字段不允许更新");
                    }
                }
            }
            // 执行写入前方法
            $this->writeBefore($param);
            // 更新数据
            $flag = (bool)$row->save($param);
            // 执行后置回调函数
            if ($afterFun) $afterFun($flag, $row, $param);
            Db::commit();
        } catch (\Exception $e) {
            Db::rollback();
            $this->error('更新失败:' . $e->getMessage());
        }
        $flag ? $this->success(null, '更新成功') : $this->error('更新失败');
    }

    /**
     * 删除
     * @param array|null $forbidWhere 禁止查询条件
     * @param callable|null $beforeFun 删除前回调函数
     * @param callable|null $afterFun 删除后回调函数
     * @return void 响应输出
     */
    protected function crudDelete(array $forbidWhere = null, callable $beforeFun = null, callable $afterFun = null): void
    {
        $KEY = $this->primaryKey;
        $param = $this->check([$KEY], 'delete');
        empty($param) && $this->error('请求参数不能为空');
        try {
            $flag = false;
            // 执行删除前回调函数
            if ($beforeFun) $param = $beforeFun($param);
            $ids = $param[$KEY];
            !is_array($ids) && $ids = explode(',', $ids); // 非数组转数组
            if (count($ids) == 1) {
                $id = $ids[0];
                $row = $this->model->where($KEY, $id)->find();
                if (empty($row)) throw new Exception('数据不存在');
                // 检查是否禁止删除
                if ($forbidWhere) {
                    $check = $this->model->where($forbidWhere)->where($KEY, $id)->find();
                    if ($check) throw new Exception('数据不存在');
                }
                $flag = (bool)$row->delete();
                // 执行删除后回调函数
                if ($afterFun) $afterFun($flag);
            } else {
                $rows = $this->model->whereIn($KEY, $ids)->select();
                if ($rows->isEmpty()) throw new Exception('数据不存在');
                // 检查是否禁止删除
                if ($forbidWhere) {
                    $idArr = $this->model->where($forbidWhere)->column($KEY);
                    array_intersect($idArr, $ids) && throw new Exception('部分数据不可删除');
                }
                // 启动事务
                $isError = false;
                Db::startTrans();
                try {
                    foreach ($rows as $row) {
                        if (!$row->delete()) $isError = true;
                    }
                    if (!$isError) {
                        $flag = true;
                        // 提交事务
                        Db::commit();
                    }
                    // 执行删除后回调函数
                    if ($afterFun) $afterFun($flag);
                } catch (\Exception $e) {
                    // 回滚事务
                    Db::rollback();
                    throw new Exception($e->getMessage());
                }
            }
        } catch (\Exception $e) {
            $this->error('删除失败:' . $e->getMessage());
        }
        $flag ? $this->success(null, '删除成功') : $this->error('删除失败');
    }

    /**
     * 详情
     * @param callable|null $afterFun
     * @return void 响应输出
     */
    protected function crudInfo(callable $afterFun = null): void
    {
        $param = $this->check([$this->primaryKey]);
        empty($param) && $this->error('请求参数不能为空');
        try {
            // 查询数据
            $row = $this->model->find($param[$this->primaryKey]);
            if (empty($row)) throw new Exception('数据不存在');
            // 执行后置回调函数
            if ($afterFun) $row = $afterFun($row);
        } catch (\Exception $e) {
            $this->error($e->getMessage());
        }
        $this->success($row);
    }

    /**
     * 修改字段数据
     * @param mixed $field 修改字段
     * @param array|null $forbidWhere 禁止修改条件
     * @param callable|null $beforeFun 修改字段前回调函数
     * @param callable|null $afterFun 修改字段后回调函数
     * @return void 响应输出
     */
    protected function crudModify(array|string $field, array $forbidWhere = null, callable $beforeFun = null, callable $afterFun = null): void
    {
        $rule = is_array($field) ? array_merge([$this->primaryKey], $field) : [$this->primaryKey, $field];
        $param = $this->check($rule, 'post');
        empty($param) && $this->error('请求参数不能为空');
        try {
            // 执行修改前回调函数
            if ($beforeFun) $param = $beforeFun($param);
            $row = $this->model->where($this->primaryKey, $param[$this->primaryKey])->find();
            if (empty($row)) throw new Exception('数据不存在');
            // 检查是否禁止修改
            if ($forbidWhere) {
                $check = $this->model->where($this->primaryKey, $param[$this->primaryKey])->where($forbidWhere)->find();
                $check && throw new Exception('数据不可修改');
            }
            $saveData = [];
            if (is_array($field)) {
                foreach ($field as $item) $saveData[$item] = $param[$item];
            } else {
                $saveData[$field] = $param[$field];
            }
            $flag = (bool)$row->save($saveData);
            // 执行修改后回调函数
            if ($afterFun) $afterFun($flag);
        } catch (\Exception $e) {
            $this->error('修改失败:' . $e->getMessage());
        }
        $flag ? $this->success(null, '修改成功') : $this->error('修改失败');
    }

    /**
     * 默认
     * @param string $field 默认字段
     * @return void 响应输出
     */
    protected function crudDefault(string $field = 'is_default'): void
    {
        $id = $this->primaryKey;
        $param = $this->check([$id], 'post');
        empty($param) && $this->error('请求参数不能为空');
        try {
            $row = $this->model->find($param[$id]);
            if (empty($row)) throw new Exception('数据不存在');
            $flag = (bool)$row->save([$field => 1]);
            $where[] = [$id, '<>', $param[$id]];
            if (!empty($this->foreignKey)) {
                foreach ($this->foreignKey as $key => $value) {
                    $where[] = [$key, '=', $value];
                }
            }
            $this->model->update([$field => 0], $where);
        } catch (\Exception $e) {
            $this->error('默认失败:' . $e->getMessage());
        }
        $flag ? $this->success(null, '默认成功') : $this->error('默认失败');
    }

    /**
     * 数值计算
     * @param string $field 字段名称
     * @param int $step 步长(不能为0,正加负减)
     * @return void 非响应输出
     */
    protected function crudMath(string $field, int $step = 1): void
    {
        $id = $this->primaryKey;
        $param = $this->check([$id], 'post');
        empty($param) && $this->error('请求参数不能为空');
        try {
            $row = $this->model->find($param[$id]);
            if (empty($row)) throw new Exception('数据不存在');
            if ($step > 0) {
                $row->inc($field, $step)->update();
            } elseif ($step < 0) {
                $row->dec($field, abs($step))->update();
            } else {
                throw new Exception('步长不能为0');
            }
        } catch (\Exception $e) {
            $this->error('操作失败:' . $e->getMessage());
        }
    }

    /**
     * 树形
     * @return void 响应输出
     */
    protected function crudTree(array $onlyFields = []): void
    {
        try {
            // 附加外键查询条件到配置查询条件中
            if ($this->foreignKey) foreach ($this->foreignKey as $key => $value) $this->where[] = [$key, '=', $value];
            // 构建条件查询器
            $query = $this->model->where($this->where);
            // 查询总条数
            $count = $query->count();
            // 查询数据
            if ($onlyFields) {
                $onlyFields = array_unique(array_merge($onlyFields, ['id,pid']));
                $list = $query->field($onlyFields)->order($this->sort)->select()->toArray();
            } else {
                $list = $query->order($this->sort)->select()->toArray();
            }
            $tree = TreeUtil::arrayToTree($list);
        } catch (\Exception $e) {
            $this->error($e->getMessage());
        }
        $this->success(['count' => $count, 'list' => $tree]);
    }

    /**
     * 树形排序
     * @return void
     */
    protected function crudTreeSort(): void
    {
        $param = $this->check(['before_id', 'after_id', 'pos_type'], 'post');
        empty($param) && $this->error('请求参数不能为空');
        try {
            if (!in_array($param['pos_type'], ['before', 'after', 'inner'])) throw new Exception('pos_type参数错误');
            $from = $this->model->field('id,pid')->find($param['before_id']); // 移动节点
            $to = $this->model->field('id,pid')->find($param['after_id']); // 目标节点
            if (empty($from)) throw new Exception('before_id数据不存在');
            if (empty($to)) throw new Exception('after_id数据不存在');
            if ($param['pos_type'] == 'inner') {
                // 移动节点到內部
                $from->save(['pid' => $to->id]);
            } else {
                if ($from->pid != $to->pid) {
                    // 移动节点到外部
                    $from->save(['pid' => $to->pid, 'sort' => $to->sort]);
                } else {
                    // 附加外键查询条件到配置查询条件中
                    if ($this->foreignKey) foreach ($this->foreignKey as $key => $value) $this->where[] = [$key, '=', $value];
                    // 查询出移动节点的同级其他节点
                    $list = $this->model
                        ->where($this->where)
                        ->where('id', '<>', $from->id)
                        ->where('pid', $to->pid)
                        ->order(['sort' => 'asc', 'id' => 'asc'])
                        ->select();
                    $ids = $list->column('id');
                    $index = array_search($to['id'], $ids);
                    if (!$list->isEmpty()) {
                        $skip = 0;
                        foreach ($list as $key => $item) {
                            if ($param['pos_type'] == 'before') {
                                if ($key == $index) {
                                    $from->save(['sort' => $key]);
                                    $skip = 1;
                                }
                                $item->save(['sort' => $key + $skip]);
                            }
                            if ($param['pos_type'] == 'after') {
                                if ($key == $index) {
                                    $item->save(['sort' => $key]);
                                    $from->save(['sort' => $key + 1]);
                                    $skip = 1;
                                } else {
                                    $item->save(['sort' => $key + $skip]);
                                }
                            }
                        }
                    }
                }
            }
        } catch (\Exception $e) {
            $this->error('操作失败:' . $e->getMessage());
        }
        $this->success(null, '操作成功');
    }
}